# -*- coding: utf-8 -*-

from google.appengine.ext import db
from google.appengine.api.namespace_manager import get_namespace

from datetime import datetime
from itertools import chain
from dicthash.utils import subdict

from .exceptions import OrphanEntityError, BadParentError, AuthorizationError, \
                        NamespaceIsNotSet, DuplicateTagError, DuplicatePubDateError, \
                        MaximumTagLengthError


MAXIMUM_TAG_LENGTH = 100


def authorize(f):
    """ """
    def inner(self, *args, **kw):
        
        current_namespace = get_namespace()
        
        if self.is_saved():
            # authorization
            if self.key().namespace() != current_namespace:
                raise AuthorizationError("Entity does not belong to current namespace")
        else:
            # ensure that current namespace is set
            if not current_namespace:
                raise NamespaceIsNotSet
            
            # When an application creates a new Key from an ancestor,
            # the new key inherits the namespace of the ancestor. (goo.gl/RuZX6)
            
            parent_key = self.parent_key()
            if parent_key and parent_key.namespace() != current_namespace:
                raise AuthorizationError("Parent entity does not belong to current namespace")
            
        return f(self, *args, **kw)
    return inner


def check_parent(f):
    """ """
    def inner(self, *args, **kw):
        cls = self.__class__
        # request to datastore
        parent = self.parent()
        #
        if parent is None:
            raise OrphanEntityError("Entities of %s kind must have a parent entity of kind `Lenta`" % cls)
            
        if type(parent) != Lenta:
            raise BadParentError("Entities of %s kind must have a parent entity of kind `Lenta`" % cls)
            
        return f(self, *args, **kw)
    return inner    


def must_be_saved(f):
    def inner(self, *args, **kw):
        if not self.is_saved():
            raise db.NotSavedError
        return f(self, *args, **kw)
    return inner
 

class BaseModel(db.Model):
    """
    """
    _manual_updatable_properties = ()
    
    @property
    def id(self):
        return self.key().id()
    
    @property
    def namespace(self):
        return self.key().namespace()
    
    @property
    def owner_key(self):
        return db.Key.from_path('User', self.key().namespace())
    
    @must_be_saved
    def update(self, **kw):
        """ """
        self._update_properties(**kw)
        self.put()
        return self
    
    def _update_properties(self, **kw):
        """
        Properties that are not listed in `_manual_updatable_properties` tuple are silenly skipped.
        """
        for prop_name, prop_value in subdict(kw, self.__class__._manual_updatable_properties).iteritems():
            setattr(self, prop_name, prop_value)
    

class Lenta(BaseModel):
    """
    Like blog concept.
    """
    _manual_updatable_properties = ('name', 'desc')
    
    name = db.StringProperty(100, required=True)
    desc = db.TextProperty(required=True)
    noteCounter = db.IntegerProperty(default=0)
    
    @classmethod
    def new(cls, **kw):
        """ """
        return cls(**kw)
    
    @authorize
    def put(self):
        return super(Lenta, self).put()
    
    @authorize
    @must_be_saved
    def delete(self):
        """
        Deletes whole entity group in a single transaction.
        """
        db.run_in_transaction(self._delete_entity_group)
        
    def _delete_entity_group(self):
        """ """
        db.delete(db.Query(keys_only=True).ancestor(self))
        super(Lenta, self).delete()  
    

class LentaKid(BaseModel):
    """ """
    @property
    def lenta(self):
        """ """
        return self.parent()
    
    @property
    def lenta_key(self):
        """ """
        return self.parent_key()
    
    @property
    def lenta_id(self):
        """ """
        return self.parent_key().id()
    

# Note    

class Note(LentaKid):
    """
    Like blogpost concept.
    """
    _manual_updatable_properties = ('title', 'twit', 'body', 'pubDate', 'isDraft', 'tags')
    
    title = db.StringProperty(100, required=True)
    twit = db.StringProperty(140, required=True)
    body = db.TextProperty(required=True)
    pubDate = db.DateTimeProperty()
    isDraft = db.BooleanProperty(required=True, default=False)
    tags = db.StringListProperty()
    
    @classmethod
    def new(cls, **kw):
        """
        Factory for creating instances. 
        """
        return cls(**kw)
    
    @authorize
    @must_be_saved
    def delete(self):
        """ """
        
        # We need to delete in transaction because `Tag` and `Lenta` entities
        # are involved in process of deleting `Note` entity.
        
        return db.run_in_transaction(self._delete_in_transaction)
    
    @authorize    
    def put(self):
        """
        """
                    
        # We need run writing to db in transaction because `Tag` and `Lenta` entities
        # are involved in process of creating or updating `Note` entity.     
        
        return db.run_in_transaction(self._put_in_transaction)
    
    @check_parent
    def _put_in_transaction(self):
        """ """
        if not self.is_saved():
        # if new note is added
            self.pubDate = (self.pubDate or datetime.now()).replace(microsecond=0)
            self.__check_pubDate_add()
            self._put_add()
        else:
        # if note is updated
            note_from_db = Note.get(self.key())
            self.pubDate = (self.pubDate or note_from_db.pubDate).replace(microsecond=0)
            self.__check_pubDate_update()
            self._put_update(note_from_db)
    
    def _put_add(self):
        """
        Add new note.
        """
        # add/update `Tag` entities
        tag_objects_to_add = self._proceed_tags_to_add(self.tags)
        
        # update parent `Lenta` entity
        self.lenta.noteCounter += 1
        
        # persist `Tag` and `Lenta` entities
        db.put(chain(tag_objects_to_add, [self.lenta]))
        
        return super(Note, self).put()
    
    def _put_update(self, self_from_db):
        """
        Update note.
        """
        self_tags = set(self.tags) #tags of updated note
        self_tags_from_db = set(self_from_db.tags) #tags of note from db
        
        # persist `Tag` entities
        db.put(chain(
            self._proceed_tags_to_add(self_tags - self_tags_from_db),
            self._proceed_tags_to_rem(self_tags_from_db - self_tags)
        ))
        
        return super(Note, self).put()
        
    def _proceed_tags_to_add(self, tag_list):
        """
        Updates or/and adds `Tag`'s entities that are added for a given `Note` entity.
        Returns iterable object containing updated or/and added `Tag`'s entities. 
        """
        lenta = self.lenta
        
        tag_objects_from_db = [tag for tag in Tag.get_by_tag_string(tag_list, lenta) if tag is not None]
        # brand new tags
        new_tag_set = set(tag_list) - set([tag.key().name() for tag in tag_objects_from_db])
        
        # updating counter for existings tags
        for tag_obj in tag_objects_from_db:
            tag_obj.counter += 1
        # creating new tag objects    
        new_tag_objects = [Tag.new(tag, counter=1, parent=lenta) for tag in new_tag_set]
        
        return chain(tag_objects_from_db, new_tag_objects)      
       
    def _proceed_tags_to_rem(self, tag_list):
        """
        Updates `Tag`'s entities that are removed for a given entity.
        Returns iterable object containing `Tag`'s entities. 
        """
        tag_objects_from_db = [tag for tag in Tag.get_by_tag_string(tag_list, self.lenta) if tag is not None]
         
        for tag_obj in tag_objects_from_db:
            tag_obj.counter -= 1  
            
        return tag_objects_from_db
    
    def _delete_in_transaction(self):
        """ """
        note_from_db = Note.get(self.key())
        
        self.lenta.noteCounter -= 1
        
        db.put(chain(self._proceed_tags_to_rem(note_from_db.tags), [self.lenta]))
        db.delete(self)
    
    
    def __check_pubDate_add(self):
        """
        `PubDate` property must be unique across all `Note` entities.
        """
        res = self.__class__.all().ancestor(self.lenta_key).filter('pubDate =', self.pubDate).fetch(2)
        assert len(res) < 2
        if res: raise DuplicatePubDateError
        
    def __check_pubDate_update(self):
        """
        `PubDate` property must be unique across all `Note` entities.
        """
        res = self.__class__.all().ancestor(self.lenta_key).filter('pubDate =', self.pubDate).fetch(2)
        assert len(res) < 2
        if res and res[0].key() != self.key(): raise DuplicatePubDateError     
    

# Tag            

class Tag(LentaKid):
    """
    Tag is tag.
    """
    counter = db.IntegerProperty(default=0)
    
    @property
    def name(self):
        return self.key().name()
    
    @classmethod
    def get_by_tag_string(cls, tag_strings, lenta_key):
        """
        Get tag by it's name. Within given entity group.
        """
        return cls.get_by_key_name(tag_strings, parent=lenta_key)
    
    @classmethod
    def new(cls, tag_name, **kw):
        """
        Factory for creating `Tag` instances.
        """
        if len(tag_name) > MAXIMUM_TAG_LENGTH:
            raise MaximumTagLengthError
        return cls(key_name=tag_name, **kw)
    
    @authorize
    def put(self):
        return db.run_in_transaction(self._put_in_transaction)
    
    @check_parent
    def _put_in_transaction(self):
        """ """
        if len(self.key().name()) > MAXIMUM_TAG_LENGTH:
            raise MaximumTagLengthError 
        
        if not self.is_saved():
            if Tag.get_by_tag_string(self.name, self.lenta_key):
                raise DuplicateTagError("Tag entity '%s' already exists." % self.name)
        return super(Tag, self).put()
    
    @authorize
    @must_be_saved
    def update(self, new_name):
        """
        Update `Tag` entity and update relevant `Note` entities within a single transaction.
        """
        return db.run_in_transaction(self._update_tag_entity, new_name)
    
    @authorize
    @must_be_saved
    def delete(self):
        """
        Delete `Tag` entity and update relevant `Note` entities within a single transaction.
        """
        db.run_in_transaction(self._delete_tag_entity)
        
    def _update_tag_entity(self, new_name):
        """ """
        if new_name == self.name:
            return
        
        query = Note.all().ancestor(self.lenta_key).filter('tags =', self.name)
        notes_to_update = []
        
        for note in query:
            note_tags = note.tags
            note_tags[note_tags.index(self.name)] = new_name
            notes_to_update.append(note)
        
        other_tag_with_same_name = Tag.get_by_tag_string(new_name, self.lenta_key)
        
        if other_tag_with_same_name:
            _tag = other_tag_with_same_name
            _tag.counter += self.counter
        else:
            _tag = Tag.new(new_name, counter=self.counter, parent=self.lenta_key)
        
        db.put(chain(notes_to_update, [_tag]))
        super(Tag, self).delete()
        
        return _tag
        
    def _delete_tag_entity(self):
        """ """
        query = Note.all().ancestor(self.lenta).filter('tags =', self.name)
        notes_to_update = []
        
        for note in query:
            note.tags.remove(self.name)
            notes_to_update.append(note)
            
        db.put(notes_to_update)
        super(Tag, self).delete()       
    